'''
SmaCross            双均线策略
BacktestConfig      设置投资金额,佣金,分析及打印分析结果
'''
from datetime import datetime
import backtrader as bt
import pandas as pd
# Create a subclass of Strategy to define the indicators and logic
class SmaCross(bt.Strategy):
    # list of parameters which are configurable for the strategy
    # ("maperiod", 20),
    params = (
        ("pfast", 1),  # period for the fast moving average
        ("pslow",20),   # period for the slow moving average
    )

    def __init__(self):
        #==============复制粘贴=========================
        #self.dataclose = self.datas[0].close  
        # 止损比例
        self.stop_loss=0.00
        # 记录买入价格
        self.buy_price = None
        # 纪录交易执行方向, 打印输出
        self.print_list = [] #储存交易信息
        self.ordersta=''# 纪录交易执行方向
        
        self.sma1 = bt.ind.SMA(period=self.p.pfast)  # fast moving average
        self.sma2 = bt.ind.SMA(period=self.p.pslow)  # slow moving average
        # self.sma1 = bt.indicators.SimpleMovingAverage(
        #     self.datas[0], period=self.params.pfast
        # )
        # self.sma2 = bt.indicators.SimpleMovingAverage(
        #     self.datas[0], period=self.params.pslow
        # )
                
        self.crossover = bt.ind.CrossOver(self.sma1, self.sma2)  # crossover signal
        # 将 crossover 信号添加为多头信号
        # self.signal_add(bt.SIGNAL_LONG, self.crossover)
        

    def next(self):
        #==============复制粘贴=========================
        # # 计算止损价格
        # # macd零上零下区别对待
        # if self.macd_line < 0:
        #     self.stop_loss=0.01
        # else:
        #     self.stop_loss=0.03
        # stop_price = self.buy_price * (1 - self.stop_loss) if self.buy_price else 0
        
        if not self.position:  # not in the market
            if self.crossover > 0:  # if fast crosses slow to the upside
                # 更新买入价格
                self.buy_price = self.data.close[0]

                self.ordersta='买入'
                # 获取当前账户总价值的方法。通过乘以0.98，代码计算出账户总价值的98%。这个值通常用于确定下单的金额或数量，以确保不会超过账户总价值的某个百分比。
                order_value = self.broker.getvalue() * 0.90
                order_amount = self.downcast(order_value / self.data.close[0], 100)
                #半仓
                order_amount=order_amount # 执行买入
                self.order = self.buy(self.datas[0], order_amount, name=self.datas[0]._name)

        elif self.crossover < 0:  # in the market & cross to the downside
                # 执行卖出
                self.ordersta=('卖出') 
                self.order = self.order_target_percent(self.datas[0], 0, name=self.datas[0]._name)
           
    # ================可买入资金========================================================================
    def downcast(self, amount, lot): #可买入资金
	    #使用整数除法 amount // lot 来找到 amount 可以被 lot 整除的最大次数。
        return abs(amount // lot * lot) 
    
    # ==================打印信息============================================
    def notify_order(self, order): # 用于通知和处理订单信息。
        # 检查order.status是否属于两个可能的状态之一：order.Submitted 和 order.Accepted。
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            if order.isbuy():
                """self.log(
                    f"买入{order.info['name']}, 成交量{order.executed.size}，成交价{order.executed.price:.2f} 订单状态：{order.status}")
                self.log('买入后当前资产：%.2f 元' % self.broker.getvalue())"""
                self.log(
                            f"执行:{self.ordersta},"
                            f"代码:{order.info['name']},"
                            f"成交量:{order.executed.size},"
                            f"成交价:{order.executed.price:.2f},"
                            f"当前资产:{self.broker.getvalue():.2f}"
                        )               
            elif order.issell():
                """self.log(
                    f"卖出{order.info['name']}, 成交量{order.executed.size}，成交价{order.executed.price:.2f} 订单状态：{order.status}")
                self.log('卖出后当前资产：%.2f 元' % self.broker.getvalue()) """
                self.log(
                            f"执行:{self.ordersta},"
                            f"代码:{order.info['name']},"
                            f"成交量:{order.executed.size},"
                            f"成交价:{order.executed.price:.2f}," 
                            f"当前资产:{self.broker.getvalue():.2f}"
                        )                
                
            self.bar_executed = len(self)

        # Write down: no pending order
        self.order = None

    def log(self, txt, dt=None): #日志-日期和信息
        """
        输出日期
        :param txt:
        :param dt:
        :return:
        """
        dt = dt or self.datetime.date(0)  # 现在的日期
        #'%s , %s' 是一个字符串模板，其中%s是一个占位符，用于表示一个字符串。
        # 在这个模板中，有两个%s，这意味着我们需要为它们提供两个字符串值。
        # (dt.isoformat(), txt) 是一个元组，它包含了两个值：dt.isoformat() 和 txt。
        # isoformat方法默认会生成一个包含T的ISO 8601格式的字符串（如2023-10-23T14:30:00），
        # 所以在输出的日期和时间之间会有一个T
        print('%s , %s' % (dt.isoformat(), txt))
        self.print_list.append('日期: %s , %s' % (dt.isoformat(), txt))#加入到打印列表,最后一起打印,输出csv
    pass

    def notify_trade(self, trade): #打印交易信息
        '''可选，打印交易信息'''
        pass

    def get_print_list(self):
        return self.print_list 
       
#==================自定义一个 CommissionInfo 类来处理滑点。===============================================
class CustomCommissionInfo(bt.CommissionInfo):
    def __init__(self, *args, **kwargs):
        self.slippage = kwargs.pop('slippage', 0.0)
        super(CustomCommissionInfo, self).__init__(*args, **kwargs)

    def get_operationcost(self, size, price):
        cost = super(CustomCommissionInfo, self).get_operationcost(size, price)
        slippage_cost = abs(size) * self.slippage
        return cost + slippage_cost
# ========================================================================
class BacktestConfig:
    '''建立类(设置投资金额,佣金,分析报告及打印分析结果)'''
    def __init__(self, start_cash=10000, commission=0.002, slippage=0.005):
        self.start_cash = start_cash
        self.commission = commission
        self.slippage=slippage

    def configure_broker(self, cerebro):
        '''设置投资金额,佣金'''
        cerebro.broker.setcash(self.start_cash)
        cerebro.broker.setcommission(commission=self.commission)
        # 设置滑点
        cerebro.broker.addcommissioninfo(CustomCommissionInfo(commission=self.commission, slippage=self.slippage))
        
        
    def add_analyzers(self, cerebro):
        cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='tradeanalyzer')
        cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='annualReturn')
        cerebro.addanalyzer(bt.analyzers.Returns, _name='annualizedReturns', tann=252)
        cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
        cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpeRatio')
        cerebro.addanalyzer(bt.analyzers.Returns, _name='totalReturns')
        cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='timeReturn')

    def setup(self, cerebro):
        self.configure_broker(cerebro)
        self.add_analyzers(cerebro)

    def print_results(self, cerebro, results, start_date, end_date):
        port_value = cerebro.broker.getvalue()
        pnl = port_value - self.start_cash
        print(f"初始资金: {self.start_cash}\n回测期间：{start_date.strftime('%Y%m%d')}:{end_date.strftime('%Y%m%d')}")
        print(f"总资金: {round(port_value, 2)}")
        print(f"净收益: {round(pnl, 2)}")

        total_trades = results[0].analyzers.tradeanalyzer.get_analysis()['total']['total']
        won_trades = results[0].analyzers.tradeanalyzer.get_analysis()['won']['total']
        win_rate = (won_trades / total_trades) * 100 if total_trades > 0 else 0
        print('总交易次数:', total_trades)
        print('盈利次数:', won_trades)
        print('胜率%:', win_rate)

        print('年化收益%:', results[0].analyzers.annualizedReturns.get_analysis()['rnorm100'])
        print('最大回撤比例%:', results[0].analyzers.drawdown.get_analysis().max.drawdown)
        print('夏普比率:', results[0].analyzers.sharpeRatio.get_analysis()['sharperatio'])
        print('累计收益：', results[0].analyzers.totalReturns.get_analysis()['rtot'])

        print(f'最后投资金额：{round(cerebro.broker.getvalue(), 2)}')
        
        #===打印年报,回测结果中提取年化收益率的分析数据。========
        # 假设 annual_return_data 是您的 OrderedDict 数据  
        annual_return_data =  results[0].analyzers.annualReturn.get_analysis()
        
        # 设定年份字段的最小宽度（不包括负号）和小数点后的位数  
        year_field_width = 4  # 假设年份最多4位  
        decimal_places = 8  # 小数点后的位数  
        
        # 使用列表推导式来格式化字符串，确保小数点对齐  
        # 对于年份，我们不需要特殊处理，直接格式化即可  
        # 对于回报值，我们使用字符串格式化来确保它有一个固定的宽度，包括小数点和小数部分  
        lines = [f"{year:>{year_field_width}d}, {return_value:+.{decimal_places}f}" for year, return_value in annual_return_data.items()]  
        
        # 使用 '\n' 拼接这些行以换行  
        output = '\n'.join(lines)  
        
        # 打印结果  
        print('年度汇报:\n' + output)
    
    def excel_results(self,cerebro,results, start_date, end_date):
        # ============================分析报告================================
        port_value = cerebro.broker.getvalue()
        pnl = port_value - self.start_cash
        total_trades = results[0].analyzers.tradeanalyzer.get_analysis()['total']['total']
        won_trades = results[0].analyzers.tradeanalyzer.get_analysis()['won']['total']
        win_rate = (won_trades / total_trades) * 100 if total_trades > 0 else 0
        annualized_return = results[0].analyzers.annualizedReturns.get_analysis()['rnorm100']
        max_drawdown = results[0].analyzers.drawdown.get_analysis().max.drawdown
        sharpe_ratio = results[0].analyzers.sharpeRatio.get_analysis()['sharperatio']
        cumulative_return = results[0].analyzers.totalReturns.get_analysis()['rtot']
        results_list = [
            ["初始资金", self.start_cash],
            ["回测期间", f"{start_date.strftime('%Y%m%d')}:{end_date.strftime('%Y%m%d')}"],
            ["总资金", round(port_value, 2)],
            ["净收益", round(pnl, 2)],
            ["总交易次数", total_trades],
            ["盈利次数", won_trades],
            ["胜率%", win_rate],
            ["年化收益%", annualized_return],
            ["最大回撤比例%", max_drawdown],
            ["夏普比率", sharpe_ratio],
            ["累计收益", cumulative_return],
            ["最后投资金额", round(cerebro.broker.getvalue(), 2)]
        ]

        # 将结果转换为DataFrame
        df_results = pd.DataFrame(results_list, columns=["指标", "值"])

        # 将DataFrame保存为Excel文件
        df_results.to_excel('results.xlsx', index=False)
        
        # ===============成交回报===================================
        # 获取策略实例
        strategy_instance = results[0]

        # 调用策略实例的get_print_list方法获取print_list
        # 调出成交列表
        print_list = strategy_instance.get_print_list()

        def parse_data_strings_to_df(data_strings):  # 成交列表转成dataframe
            data_rows = []  
            for data_str in data_strings:  
                parts = data_str.split(',')  # 按逗号分割字符串  
                row_data = {}  
                for part in parts:  
                    key_value = part.split(':')  # 尝试按冒号分割键值对  
                    if len(key_value) == 2:  
                        key, value = key_value  
                        row_data[key.strip()] = value.strip()  # 去除键和值两侧的空白字符  
                    else:  
                        print(f"Warning: Invalid key-value pair: {part}")  
                data_rows.append(row_data)  
            return pd.DataFrame(data_rows)  
        
        # 创建一个DataFrame  
        df_print_list = parse_data_strings_to_df(print_list) 
                # 将两个 DataFrame 并排合并成一个 DataFrame
        df_combined = pd.concat([df_print_list, df_results], axis=1)

        # 将合并后的 DataFrame 保存为 Excel 文件
        df_combined.to_excel('backtrader\技术指标\均线\combined_results2.xlsx', index=False)

        print("结果已成功写入 combined_results2.xlsx 文件")
        

# 使用示例
if __name__ == "__main__":
    print("这是库文件")
    print("这是库文件")
    
    print("这是库文件")
    '''
    cerebro = bt.Cerebro()
    config = BacktestConfig()
    config.setup(cerebro)

    # 继续添加数据和策略等其他设置
    # cerebro.adddata(data)
    # cerebro.addstrategy(SmaCross, pfast=50, pslow=100) 

    # 运行回测
    results = cerebro.run()

    # 打印结果
    start_date = datetime(2023, 5, 1)
    end_date = datetime(2023, 10, 1)
    config.print_results(cerebro, results, start_date, end_date)
    '''